package mysql

import (
	"database/sql"
	"errors"
	"github.com/didi/gendry/builder"
	"github.com/didi/gendry/scanner"
	"reflect"
)

type TxManagement struct {
	tx *sql.Tx
}

func (t *TxManagement) Close() error {
	if t.tx != nil {
		return t.tx.Commit()
	}
	return nil
}

func NewTxManagement(tx *sql.Tx) *TxManagement {
	return &TxManagement{
		tx: tx,
	}
}

// Query
// For query parameters, see where comments
func (t *TxManagement) Query(table string, where map[string]interface{}, result interface{}) (err error) {
	cond, values, err := builder.BuildSelect(table, where, []string{})
	if err != nil {
		return
	}

	rows, err := t.tx.Query(cond, values...)
	defer rawsClose(rows)
	if err != nil {
		return
	}

	err = scanner.Scan(rows, result)
	return
}

// QuerySql
// sql := "select * from tb where name={{name}} and id in (select uid from anothertable where score in {{m_score}})"
// data := map[string]interface{}{"name": "mike", "m_score": []float64{1.2, 2.3}}
func (t *TxManagement) QuerySql(sql string, data map[string]interface{}, result interface{}) (err error) {
	cond, values, err := builder.NamedQuery(sql, data)
	if err != nil {
		return
	}

	rows, err := t.tx.Query(cond, values...)
	defer rawsClose(rows)
	if err != nil {
		return
	}

	err = scanner.Scan(rows, result)
	return
}

// QueryWithSelectFields
// Like the query method, but you can specify the fields you want to display
func (t *TxManagement) QueryWithSelectFields(table string, where map[string]interface{}, selectFields []string, result interface{}) (err error) {
	cond, values, err := builder.BuildSelect(table, where, selectFields)
	if err != nil {
		return
	}

	rows, err := t.tx.Query(cond, values...)
	defer rawsClose(rows)
	if err != nil {
		return
	}

	err = scanner.Scan(rows, result)
	return
}

// QueryLimit
// Like the query method, offset and limit are supported as separate arguments
func (t *TxManagement) QueryLimit(table string, where map[string]interface{}, offset, limit uint, result interface{}) (err error) {
	if offset < 0 || limit <= 0 {
		err = errors.New("offset or limit is invalid")
		return
	}
	where["_limit"] = []uint{offset, limit}
	cond, values, err := builder.BuildSelect(table, where, []string{})
	if err != nil {
		return
	}

	rows, err := t.tx.Query(cond, values...)
	defer rawsClose(rows)
	if err != nil {
		return
	}

	err = scanner.Scan(rows, result)
	return
}

// Update
// where likely to query where, but it doesn't support _orderby, _groupby, _having
// _limit is supported, but only an int value can be passed to represent how much data is updated
func (t *TxManagement) Update(table string, where, update map[string]interface{}) (res sql.Result, err error) {
	cond, values, err := builder.BuildUpdate(table, where, update)
	if err != nil {
		return
	}

	return t.tx.Exec(cond, values...)
}

// UpdateLimit
// where likely to query where, but it doesn't support _orderby, _groupby, _having
func (t *TxManagement) UpdateLimit(table string, where, update map[string]interface{}, limit int) (res sql.Result, err error) {
	if limit <= 0 {
		err = errors.New("limit is invalid")
		return
	}
	where["_limit"] = limit
	cond, values, err := builder.BuildUpdate(table, where, update)
	if err != nil {
		return
	}

	return t.tx.Exec(cond, values...)
}

// Check and process the inserted data
// data can be map, struct, slice of map, slice of struct
func (t *TxManagement) insertDataTreating(data interface{}) (result []map[string]interface{}, err error) {
	value := reflect.ValueOf(data)
	if value.Kind() == reflect.Map {
		if tmp, ok := value.Interface().(map[string]interface{}); ok {
			result = []map[string]interface{}{tmp}
		}
		return
	}
	if value.Kind() == reflect.Ptr {
		value = value.Elem()
		if value.Kind() != reflect.Struct {
			err = errors.New("data must be struct ptr")
			return
		}
		itemMap := make(map[string]interface{})
		itemType := value.Type()
		for i := 0; i < value.NumField(); i++ {
			field := value.Field(i)
			fieldName := itemType.Field(i).Name
			itemMap[fieldName] = field.Interface()
		}
		result = []map[string]interface{}{itemMap}
		return
	}
	if value.Kind() == reflect.Struct {
		itemMap := make(map[string]interface{})
		itemType := value.Type()
		for i := 0; i < value.NumField(); i++ {
			field := value.Field(i)
			fieldName := itemType.Field(i).Name
			itemMap[fieldName] = field.Interface()
		}
		result = []map[string]interface{}{itemMap}
		return
	}

	// data is slice
	if value.Kind() != reflect.Slice {
		err = errors.New("data must be slice")
		return
	}
	result = make([]map[string]interface{}, 0)
	for i := 0; i < value.Len(); i++ {
		itemValue := value.Index(i)
		if itemValue.IsNil() {
			continue
		}
		switch itemValue.Kind() {
		case reflect.Map:
			if tmp, ok := itemValue.Interface().(map[string]interface{}); ok {
				result = append(result, tmp)
			}
		case reflect.Struct:
			itemMap := make(map[string]interface{})
			itemType := itemValue.Type()
			for j := 0; j < itemValue.NumField(); j++ {
				field := itemValue.Field(j)
				fieldName := itemType.Field(j).Name
				itemMap[fieldName] = field.Interface()
			}
			result = append(result, itemMap)
		case reflect.Ptr:
			itemValue = itemValue.Elem()
			if itemValue.Kind() != reflect.Struct {
				continue
			}
			itemMap := make(map[string]interface{})
			itemType := itemValue.Type()
			for i := 0; i < itemValue.NumField(); i++ {
				field := itemValue.Field(i)
				fieldName := itemType.Field(i).Name
				itemMap[fieldName] = field.Interface()
			}
			result = append(result, itemMap)
		default:
			continue
		}
	}
	return
}

// Insert data
func (t *TxManagement) Insert(table string, data interface{}) (res sql.Result, err error) {
	result, err := t.insertDataTreating(data)
	if err != nil {
		return
	}

	cond, values, err := builder.BuildInsert(table, result)
	if err != nil {
		return
	}

	return t.tx.Exec(cond, values...)
}

// InsertIgnore data
func (t *TxManagement) InsertIgnore(table string, data interface{}) (res sql.Result, err error) {
	result, err := t.insertDataTreating(data)
	if err != nil {
		return
	}

	cond, values, err := builder.BuildInsertIgnore(table, result)
	if err != nil {
		return
	}

	return t.tx.Exec(cond, values...)
}

// InsertReplace data
func (t *TxManagement) InsertReplace(table string, data interface{}) (res sql.Result, err error) {
	result, err := t.insertDataTreating(data)
	if err != nil {
		return
	}

	cond, values, err := builder.BuildReplaceInsert(table, result)
	if err != nil {
		return
	}

	return t.tx.Exec(cond, values...)
}

// InsertOnDuplicate data
func (t *TxManagement) InsertOnDuplicate(table string, data interface{}, update map[string]interface{}) (res sql.Result, err error) {
	result, err := t.insertDataTreating(data)
	if err != nil {
		return
	}

	cond, values, err := builder.BuildInsertOnDuplicate(table, result, update)
	if err != nil {
		return
	}

	return t.tx.Exec(cond, values...)
}

// Delete data
func (t *TxManagement) Delete(table string, where map[string]interface{}) (res sql.Result, err error) {
	cond, values, err := builder.BuildDelete(table, where)
	if err != nil {
		return
	}

	return t.tx.Exec(cond, values...)
}
